// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:test/test.dart';

import '../../../tool/lsp_spec/codegen_dart.dart';
import '../../../tool/lsp_spec/generate_all.dart';
import '../../../tool/lsp_spec/meta_model.dart';
import 'matchers.dart';

void main() {
  group('meta model reader', () {
    setUpAll(() {
      // Ensure any custom types like LSPAny are registered so that they can
      // be resolved.
      recordTypes(getCustomClasses());
    });
    test('reads an interface', () {
      var input = {
        'structures': [
          {
            'name': 'SomeOptions',
            'properties': [
              {
                'name': 'options',
                'type': {
                  'kind': 'array',
                  'element': {'kind': 'reference', 'name': 'string'},
                },
                'optional': true,
                'documentation': 'Options used by something.',
              },
            ],
            'documentation': 'Some options.',
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(1));
      expect(output[0], const TypeMatcher<Interface>());
      var interface = output[0] as Interface;
      expect(interface.name, equals('SomeOptions'));
      expect(interface.comment, equals('Some options.'));
      expect(interface.baseTypes, hasLength(0));
      expect(interface.members, hasLength(1));
      expect(interface.members[0], const TypeMatcher<Field>());
      var field = interface.members[0] as Field;
      expect(field.name, equals('options'));
      expect(field.comment, equals('''Options used by something.'''));
      expect(field.allowsNull, isFalse);
      expect(field.allowsUndefined, isTrue);
      expect(field.type, isArrayOf(isSimpleType('string')));
    });

    test('reads an interface with a field with an inline/unnamed type', () {
      var input = {
        'structures': [
          {
            'name': 'Capabilities',
            'properties': [
              {
                'name': 'textDoc',
                'type': {
                  'kind': 'literal',
                  'value': {
                    'properties': [
                      {
                        'name': 'deprecated',
                        'type': {'kind': 'base', 'name': 'bool'},
                        'optional': true,
                      },
                    ],
                  },
                },
                'optional': true,
              },
            ],
            'documentation': 'Some options.',
          },
        ],
      };
      var output = readModel(input);
      // Length is two because we'll fabricate the type of textDoc.
      expect(output, hasLength(2));

      // Check there was a full fabricated interface for this type.
      expect(output[0], const TypeMatcher<Interface>());
      var interface = output[0] as Interface;
      expect(interface.name, equals('CapabilitiesTextDoc'));
      expect(interface.members, hasLength(1));
      expect(interface.members[0], const TypeMatcher<Field>());
      var field = interface.members[0] as Field;
      expect(field.name, equals('deprecated'));
      expect(field.allowsNull, isFalse);
      expect(field.allowsUndefined, isTrue);
      expect(field.type, isSimpleType('bool'));
      expect(field.allowsUndefined, isTrue);

      expect(output[1], const TypeMatcher<Interface>());
      interface = output[1] as Interface;
      expect(interface.name, equals('Capabilities'));
      expect(interface.members, hasLength(1));
      expect(interface.members[0], const TypeMatcher<Field>());
      field = interface.members[0] as Field;
      expect(field.name, equals('textDoc'));
      expect(field.allowsNull, isFalse);
      expect(field.type, isSimpleType('CapabilitiesTextDoc'));
    });

    test('reads an interface with multiple fields', () {
      var input = {
        'structures': [
          {
            'name': 'SomeOptions',
            'properties': [
              {
                'name': 'options0',
                'type': {'kind': 'reference', 'name': 'LSPAny'},
                'documentation': 'Options0 used by something.',
              },
              {
                'name': 'options1',
                'type': {'kind': 'reference', 'name': 'LSPAny'},
                'documentation': 'Options1 used by something.',
              },
            ],
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(1));
      expect(output[0], const TypeMatcher<Interface>());
      var interface = output[0] as Interface;
      expect(interface.members, hasLength(2));
      for (var i in [0, 1]) {
        expect(interface.members[i], const TypeMatcher<Field>());
        var field = interface.members[i] as Field;
        expect(field.name, equals('options$i'));
        expect(field.comment, equals('''Options$i used by something.'''));
      }
    });

    test('reads an interface with a map into a MapType', () {
      var input = {
        'structures': [
          {
            'name': 'WorkspaceEdit',
            'properties': [
              {
                'name': 'changes',
                'type': {
                  'kind': 'map',
                  'key': {'kind': 'base', 'name': 'string'},
                  'value': {
                    'kind': 'array',
                    'element': {'kind': 'reference', 'name': 'TextEdit'},
                  },
                },
              },
            ],
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(1));
      expect(output[0], const TypeMatcher<Interface>());
      var interface = output[0] as Interface;
      expect(interface.members, hasLength(1));
      var field = interface.members.first as Field;
      expect(field, const TypeMatcher<Field>());
      expect(field.name, equals('changes'));
      expect(
        field.type,
        isMapOf(isSimpleType('string'), isArrayOf(isSimpleType('TextEdit'))),
      );
    });

    test('flags nullable undefined values', () {
      var input = {
        'structures': [
          {
            'name': 'A',
            'properties': [
              {
                'name': 'canBeBoth',
                'type': {
                  'kind': 'or',
                  'items': [
                    {'kind': 'base', 'name': 'string'},
                    {'kind': 'base', 'name': 'null'},
                  ],
                },
                'optional': true,
              },
              {
                'name': 'canBeNeither',
                'type': {'kind': 'base', 'name': 'string'},
              },
              {
                'name': 'canBeNull',
                'type': {
                  'kind': 'or',
                  'items': [
                    {'kind': 'base', 'name': 'string'},
                    {'kind': 'base', 'name': 'null'},
                  ],
                },
              },
              {
                'name': 'canBeUndefined',
                'type': {'kind': 'base', 'name': 'string'},
                'optional': true,
              },
            ],
          },
        ],
      };
      var output = readModel(input);
      var interface = output[0] as Interface;
      expect(interface.members, hasLength(4));
      for (var m in interface.members) {
        expect(m, const TypeMatcher<Field>());
      }
      var canBeBoth = interface.members[0] as Field,
          canBeNeither = interface.members[1] as Field,
          canBeNull = interface.members[2] as Field,
          canBeUndefined = interface.members[3] as Field;
      expect(canBeNeither.allowsNull, isFalse);
      expect(canBeNeither.allowsUndefined, isFalse);
      expect(canBeNull.allowsNull, isTrue);
      expect(canBeNull.allowsUndefined, isFalse);
      expect(canBeUndefined.allowsNull, isFalse);
      expect(canBeUndefined.allowsUndefined, isTrue);
      expect(canBeBoth.allowsNull, isTrue);
      expect(canBeBoth.allowsUndefined, isTrue);
    });

    test('formats comments correctly', () {
      var input = {
        'structures': [
          {
            'name': 'A',
            'properties': [],
            'documentation': r"""
Describes the what this class in lots of words that wrap onto multiple lines that will need re-wrapping to format nicely when converted into Dart.

Blank lines should remain in-tact, as should:
  - Indented
  - Things

Some docs have:
- List items that are not indented

Sometimes after a blank line we'll have a note.

*Note* that something.""",
          },
        ],
      };
      var output = readModel(input);
      var interface = output[0] as Interface;
      expect(
        interface.comment,
        equals('''
Describes the what this class in lots of words that wrap onto multiple lines that will need re-wrapping to format nicely when converted into Dart.

Blank lines should remain in-tact, as should:
  - Indented
  - Things

Some docs have:
- List items that are not indented

Sometimes after a blank line we'll have a note.

*Note* that something.'''),
      );
    });

    test('reads a type alias', () {
      var input = {
        'typeAliases': [
          {
            'name': 'DocumentSelector',
            'type': {
              'kind': 'array',
              'element': {'kind': 'reference', 'name': 'DocumentFilter'},
            },
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(1));
      expect(output[0], const TypeMatcher<TypeAlias>());
      var typeAlias = output[0] as TypeAlias;
      expect(typeAlias.name, equals('DocumentSelector'));
      expect(typeAlias.baseType, isArrayOf(isSimpleType('DocumentFilter')));
    });

    test('reads a type alias that is a union of unnamed types', () {
      var input = {
        'typeAliases': [
          {
            'name': 'NameOrLength',
            'type': {
              'kind': 'or',
              'items': [
                {
                  'kind': 'literal',
                  'value': {
                    'properties': [
                      {
                        'name': 'name',
                        'type': {'kind': 'base', 'name': 'string'},
                      },
                    ],
                  },
                },
                {
                  'kind': 'literal',
                  'value': {
                    'properties': [
                      {
                        'name': 'length',
                        'type': {'kind': 'base', 'name': 'number'},
                      },
                    ],
                  },
                },
              ],
            },
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(3));

      // Results should be the two inline interfaces followed by the type alias.

      expect(output[0], const TypeMatcher<Interface>());
      var interface1 = output[0] as Interface;
      expect(interface1.name, equals('NameOrLength1'));
      expect(interface1.members, hasLength(1));
      expect(interface1.members[0].name, equals('name'));

      expect(output[1], const TypeMatcher<Interface>());
      var interface2 = output[1] as Interface;
      expect(interface2.name, equals('NameOrLength2'));
      expect(interface2.members, hasLength(1));
      expect(interface2.members[0].name, equals('length'));

      expect(output[2], const TypeMatcher<TypeAlias>());
      var typeAlias = output[2] as TypeAlias;
      expect(typeAlias.name, equals('NameOrLength'));
      expect(typeAlias.baseType, const TypeMatcher<UnionType>());

      // The type alias should be a union of the two types above.
      var union = typeAlias.baseType as UnionType;
      expect(union.types, hasLength(2));
      expect(union.types[0], isSimpleType(interface1.name));
      expect(union.types[1], isSimpleType(interface2.name));
    });

    test('reads a namespace of constants', () {
      var input = {
        'enumerations': [
          {
            'name': 'ResourceOperationKind',
            'type': {'kind': 'base', 'name': 'string'},
            'values': [
              {
                'name': 'Create',
                'value': 'create',
                'documentation': 'Supports creating new files and folders.',
              },
              {
                'name': 'Delete',
                'value': 'delete',
                'documentation':
                    'Supports deleting existing files and folders.',
              },
              {
                'name': 'Rename',
                'value': 'rename',
                'documentation':
                    'Supports renaming existing files and folders.',
              },
            ],
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(1));

      expect(output[0], const TypeMatcher<LspEnum>());
      var namespace = output[0] as LspEnum;
      expect(namespace.members, hasLength(3));
      for (var m in namespace.members) {
        expect(m, const TypeMatcher<Constant>());
      }
      var create = namespace.members[0] as Constant,
          delete = namespace.members[1] as Constant,
          rename = namespace.members[2] as Constant;
      expect(create.name, equals('Create'));
      expect(create.type, isSimpleType('ResourceOperationKind'));
      expect(
        create.comment,
        equals('Supports creating new files and folders.'),
      );
      expect(rename.name, equals('Rename'));
      expect(rename.type, isSimpleType('ResourceOperationKind'));
      expect(
        rename.comment,
        equals('Supports renaming existing files and folders.'),
      );
      expect(delete.name, equals('Delete'));
      expect(delete.type, isSimpleType('ResourceOperationKind'));
      expect(
        delete.comment,
        equals('Supports deleting existing files and folders.'),
      );
    });

    test('reads a tuple in an array', () {
      var input = {
        'structures': [
          {
            'name': 'SomeInformation',
            'properties': [
              {
                'name': 'label',
                'type': {
                  'kind': 'or',
                  'items': [
                    {'kind': 'base', 'name': 'string'},
                    {
                      'kind': 'tuple',
                      'items': [
                        {'kind': 'base', 'name': 'number'},
                        {'kind': 'base', 'name': 'number'},
                      ],
                    },
                  ],
                },
              },
            ],
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(1));
      expect(output[0], const TypeMatcher<Interface>());
      var interface = output[0] as Interface;
      expect(interface.members, hasLength(1));
      var field = interface.members.first as Field;
      expect(field, const TypeMatcher<Field>());
      expect(field.name, equals('label'));
      expect(field.type, const TypeMatcher<UnionType>());
      var union = field.type as UnionType;
      expect(union.types, hasLength(2));
      expect(union.types[0], isArrayOf(isSimpleType('number')));
      expect(union.types[1], isSimpleType('string'));
    });

    test('reads an union including LSPAny into a single type', () {
      var input = {
        'structures': [
          {
            'name': 'SomeInformation',
            'properties': [
              {
                'name': 'label',
                'type': {
                  'kind': 'or',
                  'items': [
                    {'kind': 'base', 'name': 'string'},
                    {'kind': 'base', 'name': 'LSPAny'},
                  ],
                },
              },
            ],
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(1));
      expect(output[0], const TypeMatcher<Interface>());
      var interface = output[0] as Interface;
      expect(interface.members, hasLength(1));
      var field = interface.members.first as Field;
      expect(field, const TypeMatcher<Field>());
      expect(field.name, equals('label'));
      expect(field.type, isSimpleType('LSPAny'));
    });

    test('reads literal string values', () {
      var input = {
        'structures': [
          {
            'name': 'MyType',
            'properties': [
              {
                'name': 'kind',
                'type': {'kind': 'stringLiteral', 'value': 'one'},
              },
            ],
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(1));
      expect(output[0], const TypeMatcher<Interface>());
      var interface = output[0] as Interface;
      expect(interface.name, equals('MyType'));
      expect(interface.members, hasLength(1));
      expect(interface.members[0], const TypeMatcher<Field>());
      var field = interface.members[0] as Field;
      expect(field.name, equals('kind'));
      expect(field.allowsNull, isFalse);
      expect(field.allowsUndefined, isFalse);
      expect(field.type, isLiteralOf(isSimpleType('string'), "'one'"));
    });

    test('reads literal union values', () {
      var input = {
        'structures': [
          {
            'name': 'MyType',
            'properties': [
              {
                'name': 'kind',
                'type': {
                  'kind': 'or',
                  'items': [
                    {'kind': 'stringLiteral', 'value': 'one'},
                    {'kind': 'stringLiteral', 'value': 'two'},
                  ],
                },
              },
            ],
          },
        ],
      };
      var output = readModel(input);
      expect(output, hasLength(1));
      expect(output[0], const TypeMatcher<Interface>());
      var interface = output[0] as Interface;
      expect(interface.name, equals('MyType'));
      expect(interface.members, hasLength(1));
      expect(interface.members[0], const TypeMatcher<Field>());
      var field = interface.members[0] as Field;
      expect(field.name, equals('kind'));
      expect(field.allowsNull, isFalse);
      expect(field.allowsUndefined, isFalse);
      expect(field.type, const TypeMatcher<LiteralUnionType>());
      var union = field.type as LiteralUnionType;
      expect(union.types, hasLength(2));
      expect(union.types[0], isLiteralOf(isSimpleType('string'), "'one'"));
      expect(union.types[1], isLiteralOf(isSimpleType('string'), "'two'"));
    });
  });
}

List<LspEntity> readModel(Map<String, dynamic> model) =>
    LspMetaModelCleaner().cleanTypes(LspMetaModelReader().readMap(model).types);
